﻿<# SILA : Shibboleth IdP Loader For ADFS
.SYNOPSIS
	SILAmain is the main process loop for the shibboleth federation metadata
.DESCRIPTION
#
# SILA is a script that I wanted to be simple for an easy adaptation.
# 
# Installation : 
# The script should be copy on the ADFS server
# A web folder should be added to the ADFS Server
# the temp folder must be set with read right for everyone
#
# Lesson learned :
# IdP should have a unique SAML signing key
# Use absolute path, and change to to script folder when running in batch.
#
# (c) CNRS - 2012
# licence : CeCiLL B

.NOTES
	jmt-01oct2012 : create
	jmt-08nov2012-0.1 : add comment for release... It is still a working job
	jmt-10/11/12-0.2 : check validUntil value
	jmt-15/11/12-0.03 : check metdata signature
	jmt-08aug13-0.4 : reset acl for each file on the web server
	jmt-08aug13-0.5 : add copy of metadatafile to the server of the farm
	jmt-08aug13-0.6 : copy RHD file to servers with a backup
	jmt-23oct13-0.7 : split the initial script to have all configuration in the original file.
					SILA.ps1 = configuration file and entry point
					SILAmain.ps1 = main program
					SILAhelper.ps1 = helper function
					handle renamed entity
					begin to remove old entities (note: parse metadat from beginning at the end)
	jmt-22nov13-0.8 : build discoveryJSON for eduGAIN
	jmt-23mai14-0.9 : Remove oldIdP, add log, skip RHD build switch

#
# toDo
# remove deleted IdP
# error catching
# send log by email
#>

#
# main
#
function main() {
<#
.SYNOPSIS
  Main function of the script, evry run begins here.
#>
    [CmdletBinding()]
    param ()

    $psHash = @{}
	# contains the IdP that will be added to ADFS
    $rhdHash = @{}
    # contains the IdPs that will be in the RHD drop down list
    $adfsHash = @{}
    # contains IdP in ADFS, this hash table is used to remove IdPs that are no more in the metdata file

	##date formats
    $isoTimeFormat ="yyyy-MM-dd HH:mm:ss"
    $isoTimeFileFormat ="yyyy-MM-dd_HHmmss"
    
	$logDate = get-date -format "yyyy-MM-dd_HHmmss"
    
    ## extention text (demande Dirk Hoffmann)
    $logFile = Join-Path $logPath ($logDate + $logSuffix)

    # add snapin
    Add-PSSnapin Microsoft.ADFS.PowerShell -EA 0
    
	# START
    $timestamp = get-date -format $isoTimeFormat
    Log-Write "Begin : $timeStamp"

    # check work path
    foreach ($serverName in $serverNames) {
        $metadataWebPath = Join-Path -path (Join-Path -path $serverName -Childpath $metadataBaseFolder) -childPath $federationName
        if (!(Test-Path -path $metadataWebPath)) {
            new-item -type directory -Force $metadataWebPath > $null
        }
    }

    #
    # fetch the federation metadata file
    #
    Log-Write "**fetch metadata file"

    # build the metadata file path and create it as empty (remove the preceding one)
    $federationMetadataPath = (Join-Path -Path $outPath -ChildPath ("federationMetadata/" + $federationName + ".xml"))
    New-Item -type file -Force $federationMetadataPath > $null

    # download the file over http, it might be usefull to connect once with the browser to get certificate and see it network path is open
    $webClient = new-object System.Net.WebClient 
    $webClient.DownloadFile($federationMetadataUrl, $federationMetadataPath) 


    # Check if metedata is not empty before process
    if ( (get-item $federationMetadataPath).Length -eq 0) {
        Log-Write "Metadata File $federationMetadataPath is empty"
        Exit 1
    }

    # Load the federation metadata xml file
    #$xmldata=[xml](get-content $federationMetadataPath -Encoding:UTF8)
    $xmldata = new-object Xml.XmlDocument
    $xmldata.PreserveWhitespace = $true
    $xmldata.Load($federationMetadataPath)

    # check metadata Valid until value
    # if older than process time we quit
    $validUntil = [DateTime]$xmldata.EntitiesDescriptor.ValidUntil

    if ($validUntil -le (Get-Date)) {
        Log-Write "Metadata file is valid until $validUntil and we are : (Get-Date)"
        Exit 1
    }

    # check metadata signature
    # from http://msdn.microsoft.com/en-us/library/system.security.cryptography.xml.signedxml.aspx
    add-type -AssemblyName system.security

    $signatureNode = $xmldata.EntitiesDescriptor.Signature
    $signedXml = New-Object System.Security.Cryptography.Xml.SignedXml($xmldata)
    $signedXml.LoadXml($signatureNode)
    $isSigned = $signedXml.CheckSignature()

    if (! $isSigned) {
        $msg = "Metadata file signature is not valid !!"
        Write-Host $msg
        Log-Error $msg
        Exit 1
    }

    #
    # put ADFS IdP in $adfsHash
    #
    $cps = get-adfsClaimsProviderTrust
    foreach ($cp in $cps) {
        $adfsHash[$cp.Identifier] = $cp.Name
    }
    foreach ($key in $adfsManualIdp) {
        $adfsHash.Remove($key)
    }

    # create discoJson file
    if ($generateDiscoJson) 
	{
        openDiscoJson
    }
	
    #
    # process the federation metadata file
    # simply chunk the federation metadata file to get each IdP metadata
    #
    Log-Write "`r`n**split metadata file"

    # Get all the Entity Descriptor node
    $entities = $xmldata.EntitiesDescriptor.EntityDescriptor

    # idpCounter to get the number of idp record
    $idpCounter = 0


    foreach ($entity in $entities) {
	    # only process IdP
        if ($entity.IDPSSODescriptor -ne $null) {
            $name = [string]$entity.Organization.OrganizationName.innerXML
            $organizationName = $name.Normalize()

            if ($organizationName.Length -gt 0) {

                # create the idp metadata file path
                $metadataFileName = (getDomain $entity.entityID) + ".xml"
                $metadataFilePath = Join-Path -Path $outPath -ChildPath $metadataFileName

			    # add xml descriptor and a comment and write the file. 
			    # this is based on what I have in RENATER metadata
                '<?xml version="1.0" encoding="UTF-8"?>' |  out-file -encoding:UTF8 $metadataFilePath
                "<!-- " + $organizationName + " -->" |  out-file -encoding:UTF8 $metadataFilePath -append
                $entity.OuterXml.ToString() | out-file -encoding:UTF8 $metadataFilePath -append

                # move the file to web server
                MoveToServer -File $metadataFileName -Source $outPath -Destination (Join-Path -path $metadataBaseFolder -childPath $federationName)

                # store the organization name and the entity ID in hashTable for the next steps
                $psHash[$organizationName] = $entity.entityID
                $rhdHash[$entity.entityID] = $organizationName

                # remove the organization name from $adfsHash
                if ($adfsHash.ContainsKey($entity.entityID) -and ($adfsHash[$entity.entityID] -eq $organizationName )) {
                    $adfsHash.Remove($entity.entityID)
                }

                # discoJson
                if ($generateDiscoJson) {
                    discoJson -entity $entity -idpCount $idpCounter
                }

                $idpCounter ++
        
                log-Write ("$idpCounter : $organizationName / " + $entity.entityID + " -> " +(getDomain $entity.entityID))
            }
            else {
                Log-Write "ERROR : entityID $entity.entityID organization name is empty !"
            }
        }
    }
    
    # close discoJson file
    if ($generateDiscoJson) {
        closeDiscoJson
    }


    #
    # clean up the dictionnary to only add new IdP as trusted claims provider
    #
    Log-Write "`r`n**Clean up the IdP hash"

    # get all adfs trusted Claims Provider
    $cpNames = get-adfsClaimsProviderTrust | select name

    foreach ($cpName in $cpNames) 
	{
        # remove existing IdP in ADFS from Federation List
        if($psHash.ContainsKey($CpName.Name)) 
		{
            $psHash.Remove($CpName.Name)
        }
    }

    #
    # Remove old IDPs
    #
    Log-Write "`r`n**Remove old IDPs"

    $oldIdpCounter = 0

    foreach ($key in $adfsHash.Keys) {
        Log-Write  "oldIdP : $key `t  $($adfsHash[$key])"
		
        Remove-ADFSClaimsProviderTrust -TargetIdentifier ($key)
		
        $oldIdpCounter ++
    }

    #
    # Using a web server allows automatic metadata refresh in ADFS, SILA should be scheduled as needed to refresh each idp metadata.
    # 
    Log-Write "`r`n**Add new IdP"

    $newIdpCounter = 0

    foreach ($hashKey in $psHash.Keys) 
	{
		Log-Write "newIdP : $hashKey `t $($psHash[$hashKey])"
		
        # build the metadata url
	    $cpMetadataUrl = $webServerURL + (ensurefILE $federationName) + '/' + (getDomain $psHash[$hashKey]) + '.xml'

	    # add the IdP
        try 
		{
        
        # if the IdP changed is name but kept his entityID, we must remove it first 
        $checkEntity = Get-ADFSClaimsProviderTrust -Identifier ($psHash[$hashKey])
        if ($checkEntity -ne $null) {
            Remove-ADFSClaimsProviderTrust -TargetIdentifier ($psHash[$hashKey])
        }
        
        Add-ADFSClaimsProviderTrust –Name "$hashKey" –MetadataUrl "$cpMetadataUrl" -SignatureAlgorithm "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
        set-ADFSClaimsProviderTrust -Targetname "$hashKey" –MonitoringEnabled $true –AutoUpdateEnabled $true -AcceptanceTransformRulesFile "$ruleFilePath"
        }
        catch 
		{
            # todo when error, also remove the idP from rhdashtable
			
            Log-Write "ERROR adding IdP name : $hashKey `t entityID : $($psHash[$hashKey])"
            Log-Write $_
            Log-Write "Command used :"
            Log-Write "Add-ADFSClaimsProviderTrust –Name ""$hashKey"" –MetadataUrl ""$cpMetadataUrl"" -SignatureAlgorithm ""http://www.w3.org/2000/09/xmldsig#rsa-sha1"""
            Log-Write "set-ADFSClaimsProviderTrust -Targetname ""$hashKey"" –MonitoringEnabled `$true –AutoUpdateEnabled `$true -AcceptanceTransformRulesFile ""$ruleFilePath"""
            Log-Write " "
        }
        $newIdpCounter ++
    }

    #
    # the RHD page is rebuild whenever a new IdP is added
    # you will have to adapt this part to comply to your policy 
    $rhdCounter = 0

	if ($generateRHD)
	{
		Log-Write "`r`n**Build RHD page"

        if ($psHash.Count -gt 0) 
		{
		
			# remove fixed value from the list, as we use a dropdown list, I wanted our internal Idp to be always first.
			$rhdHash.Remove("urn:mace:cnrs.fr:janus.dsi.cnrs.fr")
			$rhdHash = $rhdHash.GetEnumerator() | sort-object Value

			foreach ($rhd in $rhdHash) {
				$idpSelector = $idpSelector + '<asp:ListItem Value="'+ $rhd.Name +'">' + $rhd.Value + '</asp:ListItem>'
				$rhdCounter ++
			}

			# read the template file and replace [[idpSelector]] string with the dropdown value
			$rhd = [string](get-content $rhdTemplate) 
			$rhd = $rhd.Replace("[[idpSelector]]", $idpSelector)
			$rhd > (Join-Path -Path $outPath -ChildPath $rhdTemplateName)
		
			# push the RHD file to the ADFS servers
			MoveToServer -File $rhdTemplateName -Source $outPath -Destination $rhdBaseFolder -isRHD $True
		}
    }
	
	# end
    $timestamp = get-date -format $isoTimeFormat
    Log-Write "`r`nEnd : $timeStamp"

    # recap of the job
    Log-Write "------------------"
    Log-Write "Number of "
    Log-Write ("records  :  "+ $entities.Count)
    Log-Write "Idp      :  $idpCounter"
    Log-Write "old IdP  :  $oldIdpCounter"
    Log-Write "new IdP  :  $newIdpCounter"
    Log-Write "rhd item :  $rhdCounter"
	
	#
	# send log by email
	#
	MailReport
}